DestinationSet should implement java.util.Set#44
DestinationSet should implement java.util.Set#44rogin wants to merge 2 commits intoOpenIntegrationEngine:mainfrom
Conversation
|
Thanks for submitting this! I always meant to give it a more thorough review and suggest a few things. Just a heads up, this probably will not be a top priority until after we get our first release out the door. |
|
Can this be revisited now? @tonygermano |
There was a problem hiding this comment.
Pull request overview
This PR implements the java.util.Set<Integer> interface for the DestinationSet class, addressing a long-standing feature request. The implementation adds standard Set methods (add, contains, iterator, etc.) and modernizes the code with Java 8 streams and Optional.
Key Changes:
DestinationSetnow implementsSet<Integer>, enabling use as a standard Java collection- Refactored existing methods to use streams and Optional for cleaner code
- Added comprehensive test suite with 20+ test cases covering Set interface methods
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 21 comments.
| File | Description |
|---|---|
| server/src/com/mirth/connect/server/userutil/DestinationSet.java | Implements Set interface with all required methods, refactors internal logic to use streams and Optional |
| server/test/com/mirth/connect/userutil/DestinationSetTest.java | Adds comprehensive test suite for Set interface implementation and existing functionality |
Critical Issues Found:
- NullPointerException bug: The constructor leaves
metaDataIdsnull when the source map doesn't contain the required key, causing NPEs in all Set methods - Test reliability: Multiple tests assume HashSet iteration order, which is not guaranteed and could cause intermittent test failures
- Missing test coverage: No test for the scenario where the source map lacks the
DESTINATION_SET_KEY
Code Quality Issues:
- Inconsistent spacing in control flow statements (
if,for) - Unchecked casts that will generate compiler warnings
- Comment formatting inconsistencies
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| @Override | ||
| public boolean retainAll(Collection<?> metaDataIdOrConnectorNames) { | ||
| return removeAllExcept((Collection<Object>)metaDataIdOrConnectorNames); |
There was a problem hiding this comment.
Unchecked cast from Collection<?> to Collection<Object>. While this works, it will generate a compiler warning. Consider adding @SuppressWarnings("unchecked") to this method or refactoring to avoid the cast.
|
|
||
| @Override | ||
| public boolean removeAll(Collection<?> metaDataIdOrConnectorNames) { | ||
| return remove((Collection<Object>)metaDataIdOrConnectorNames); |
There was a problem hiding this comment.
Unchecked cast from Collection<?> to Collection<Object>. While this works, it will generate a compiler warning. Consider adding @SuppressWarnings("unchecked") to this method or refactoring to avoid the cast.
| @Test | ||
| public void testIsEmptyWithoutSourceMapKey() throws Exception { | ||
| DestinationSet ds = getNewTestDestinationSetUsing(new Integer[0]); | ||
|
|
||
| assertTrue(ds.isEmpty()); | ||
| assertEquals(0, ds.size()); | ||
| } |
There was a problem hiding this comment.
The test name testIsEmptyWithoutSourceMapKey is misleading. The test actually creates a source map WITH the key (line 47 in getNewTestDestinationSetUsing), just with an empty set.
There's no test coverage for the scenario where the source map does NOT contain Constants.DESTINATION_SET_KEY at all (i.e., when line 45 in DestinationSet.java returns false). This is a critical gap because that scenario will cause a NullPointerException with the current implementation.
Add a test like:
@Test
public void testWithoutSourceMapKey() throws Exception {
ConnectorMessage cm = new ConnectorMessage();
Map<String, Object> sm = new HashMap<>();
// Don't put DESTINATION_SET_KEY in the map
cm.setSourceMap(sm);
ImmutableConnectorMessage icm = new ImmutableConnectorMessage(cm);
DestinationSet ds = new DestinationSet(icm);
assertTrue(ds.isEmpty());
assertEquals(0, ds.size());
}| public void testToArrayGenericProperlySized() throws Exception { | ||
| DestinationSet ds = getNewTestDestinationSet(); | ||
|
|
||
| Integer[] arr = new Integer[ds.size()]; | ||
| //the input and response should be the same array | ||
| Integer[] resp = ds.toArray(arr); | ||
| assertEquals(3, resp.length); | ||
| assertTrue(Arrays.equals(arr, resp)); | ||
| assertEquals(Integer.valueOf(1), arr[0]); | ||
| assertEquals(Integer.valueOf(2), arr[1]); | ||
| assertEquals(Integer.valueOf(3), arr[2]); | ||
| } |
There was a problem hiding this comment.
This test assumes a specific array order (1, 2, 3), but DestinationSet uses a HashSet internally, which does not guarantee any specific order. The test is flawed and could fail intermittently.
The test should verify that all expected elements are present without assuming order.
| DestinationSet ds = getNewTestDestinationSet(); | ||
|
|
||
| Integer[] arr = new Integer[ds.size()]; | ||
| //the input and response should be the same array |
There was a problem hiding this comment.
Comment formatting should follow convention with a space after //. Should be // the input and response should be the same array.
| //the input and response should be the same array | |
| // the input and response should be the same array |
|
|
||
| assertFalse(ds.contains(0)); | ||
| assertTrue(ds.contains(1)); | ||
| assertTrue(ds.contains("Map to 2")); |
There was a problem hiding this comment.
Actual argument type 'String' is incompatible with expected argument type 'Integer'.
| assertTrue(ds.contains("Map to 2")); | |
| assertTrue(ds.contains(2)); |
| assertTrue(ds.contains("Map to 2")); | ||
| assertTrue(ds.contains(3)); | ||
| assertFalse(ds.contains(4)); | ||
| assertFalse(ds.contains("Invalid")); |
There was a problem hiding this comment.
Actual argument type 'String' is incompatible with expected argument type 'Integer'.
| * @return A boolean indicating whether at least one destination connector was actually removed | ||
| * from processing for this message. | ||
| */ | ||
| public boolean removeAllExcept(Object metaDataIdOrConnectorName) { |
There was a problem hiding this comment.
Method DestinationSet.removeAllExcept(..) could be confused with overloaded method removeAllExcept, since dispatch depends on static types.
| * @return A boolean indicating whether at least one destination connector was actually removed | ||
| * from processing for this message. | ||
| */ | ||
| public boolean remove(Object metaDataIdOrConnectorName) { |
| * destination connector name. | ||
| * @return A boolean indicating whether at least one destination connector was actually removed | ||
| * from processing for this message. | ||
| */ |
There was a problem hiding this comment.
This method overrides Set.remove; it is advisable to add an Override annotation.
| */ | |
| */ | |
| @Override |
| public void testToArray() throws Exception { | ||
| DestinationSet ds = getNewTestDestinationSet(); | ||
|
|
||
| Object[] arr = ds.toArray(); | ||
| assertEquals(3, arr.length); | ||
| assertEquals(Integer.valueOf(1), arr[0]); | ||
| assertEquals(Integer.valueOf(2), arr[1]); | ||
| assertEquals(Integer.valueOf(3), arr[2]); | ||
| } |
There was a problem hiding this comment.
This is true. Tests that assume toArray to be deterministic could be flaky.
| try { | ||
| if (connectorMessage.getSourceMap().containsKey(Constants.DESTINATION_SET_KEY)) { | ||
| this.destinationIdMap = connectorMessage.getDestinationIdMap(); | ||
| this.metaDataIds = (Set<Integer>) connectorMessage.getSourceMap().get(Constants.DESTINATION_SET_KEY); | ||
| } | ||
| } catch (Exception e) { | ||
| metaDataIds = new HashSet<>(); |
There was a problem hiding this comment.
This is also a good point and probable cause for future NPEs.
pacmano1
left a comment
There was a problem hiding this comment.
Needs more review.
"add" should be in it own PR. We should try to avoid adding features in the context of a limited scope issue.
|
I'm impressed by the copilot results. I'd like to resolve the findings this weekend. |
|
Impressive work. |
Signed-off-by: Richard Ogin <rogin@users.noreply.github.com>
3e60899
2b23e77 to
3e60899
Compare
tonygermano
left a comment
There was a problem hiding this comment.
In addition to the comments below, I pushed a change to your branch with the following:
- I fixed the line endings on DestinationSet so that you were no longer rewriting the entire file.
- I changed the header on DestinationSet and added one to DestinationSetTest in SPDX format and adding your name to the copyright.
- I rebased to resolve the merge conflict with previous PR #133 (keeps the changes in this PR, which negate the changes from the previous PR.)
The previous PR also added tests, but to file server/test/com/mirth/connect/server/userutil/DestinationSetTest.java instead of server/test/com/mirth/connect/userutil/DestinationSetTest.java. I think the other PR was correct, and this PR should add to the other tests. I think this is also what is currently causing the build to fail.
| } | ||
|
|
||
| return false; | ||
| return remove(Collections.singleton(metaDataIdOrConnectorName)); |
There was a problem hiding this comment.
suggestion: annotate with @Override
This is a Set operation
suggestion: keep this method simple and don't wrap and delegate
If metaDataIds is made final and never null above, then the first null check can be removed.
| } | ||
|
|
||
| return removed; | ||
| if(metaDataIdOrConnectorNames == null) { return false; } |
There was a problem hiding this comment.
nitpick: follow copilot recommendation to add space in if (
suggestion: move this implementation to Set operation removeAll and deprecate this method
This encourages the use of the Set operations remove for a single object and removeAll for collections. The overloaded remove has been a source of confusion in the past.
issue: should throw a NPE instead of return false when Collection is null
This matches the previous behavior and is specified by Set.removeAll
| .filter(Boolean::booleanValue) | ||
| .count() > 0; |
There was a problem hiding this comment.
| .filter(Boolean::booleanValue) | |
| .count() > 0; | |
| .anyMatch(Boolean::booleanValue) |
| } | ||
|
|
||
| return false; | ||
| return removeAllExcept(Collections.singleton(metaDataIdOrConnectorName)); |
There was a problem hiding this comment.
suggestion: make this a call to Set operation retainAll
| } | ||
|
|
||
| return false; | ||
| if(metaDataIdOrConnectorNames == null) { return false; } |
There was a problem hiding this comment.
nitpick: follow copilot recommendation to add space in if (
suggestion: move this implementation to Set operation retainAll and deprecate this method
This encourages the use of the Set operations and reduces confusion about two methods which do the same thing. I think removeAllExcept(Object) can remain since Set does not provide a retainOnly method for a single Object.
issue: should throw a NPE instead of return false when Collection is null
This matches the previous behavior and is specified by Set.retainAll
Suggestion: remove null check for metaDataIds
It should not be null if following other recommendations in this review
| } | ||
|
|
||
| @Override | ||
| public boolean addAll(Collection<? extends Integer> metaDataIdOrConnectorNames) { |
There was a problem hiding this comment.
issue: addAll should throw UnsupportedOperationException
DestinationSets do not support adding to the Set
| } | ||
|
|
||
| @Override | ||
| public boolean retainAll(Collection<?> metaDataIdOrConnectorNames) { |
There was a problem hiding this comment.
suggestion: deprecate removeAllExcept(Collection<Object>) in favor of this method
Also mentioned above. The Set operations should be the primary methods to reduce confusion and we can eventually remove the old methods.
| } | ||
|
|
||
| @Override | ||
| public boolean removeAll(Collection<?> metaDataIdOrConnectorNames) { |
There was a problem hiding this comment.
suggestion: deprecate remove(Collection<Object>) in favor of this method
Also mentioned above. The Set operations should be the primary methods to reduce confusion and we can eventually remove the old methods.
| Optional<Integer> m = convertToMetaDataId(metaDataIdOrConnectorName); | ||
|
|
||
| return m.isPresent() && metaDataIds.contains(m.get()); |
There was a problem hiding this comment.
if convertToMetaDataId is changed back to returning nulls and metaDataIds is always non-null as suggested elsewhere, I think this can be simplified to just
return metaDataIds.contains(convertToMetaDataId(metaDataIdOrConnectorName));|
|
||
| @Override | ||
| public boolean containsAll(Collection<?> metaDataIdOrConnectorNames) { | ||
| if(metaDataIdOrConnectorNames == null) { return false; } |
There was a problem hiding this comment.
nitpick: follow copilot recommendation to add space in if (
issue: Set.containsAll should throw a NPE instead of returning false when Collection is null
This was an old feature, requested by several people, that NextGen chose to ignore. Prior code review comments are here. I compiled it with Java 8, and the test ran successfully. No further testing on my part.